即便 HTTP 是無狀態的協定,但大家還是會期望驗證完了身分後,這個狀態能保存到下一個 HTTP 請求。
若是熟悉 web 開發的開發者,相信對 Cookie 並不陌生,Cookie 的出現有如幫 HTTP 加裝了狀態機一般。
與 HTTP 一樣,了解一下歷史。
從簡介 HTTP 的歷史時,可以知道 1991 年開始有了 HTTP,而在 1994 年的時候,Netscape 提出了 Cookie 的初始規格,並在 Netscape 上實現,隔年 1995 年,Internet Explorer 也開始支援。
接著在 1997 年,有了 RFC 2109 出爐,接著 2000 年發布了 RFC 2965 取代 RFC 2109,最後才是 2011 年發布大家目前所熟知的 RFC 6265。三個 RFC 標題都叫 HTTP State Management Mechanism,在定義 HTTP 如何管理狀態的機制。
全部讀完就能成為餅乾達人,但鐵人賽時間有限,先了解這三個 RFC 的關係是:舊的標記 HISTORIC,同時標記它被新的 Obsoleted,因此可以了解 RFC 2109 與 RFC 2965 是歷史文物,若要了解現在的 Cookie 機制,還是看 RFC 6265 比較清楚。
RFC 6265 的 Overview 可以了解 Cookie 的概觀。簡單來說,服務器若想在保存某個 HTTP 的狀態時,就在回應的 Header 加入 Set-Cookie
的值裡;客戶端取得回應,可以把這個值保存下來,當想繼續使用這個狀態時,就在請求的 Header 加入 Cookie
的值。
如同單元測試一般,有個測試案例將會有助於了解規格。下面的 Examples 正是一個很明確簡單的範例,它代表了 Server 回傳給 User Agent 的回應帶有 Set-Cookie
,而 User Agent 在下一次的請求裡,即可在 Cookie
夾帶著前一次回應拿到的 SID:
== Server -> User Agent ==
Set-Cookie: SID=31d4d96e407aad42
== User Agent -> Server ==
Cookie: SID=31d4d96e407aad42
時序圖如下:
@startuml
Alice -> Bob: Request
Bob --> Alice: Response: Set-Cookie: SID=31d4d96e407aad42
Alice -> Bob: Request: Cookie: SID=31d4d96e407aad42
Bob --> Alice: Response
@enduml
上面的設定,代表告訴 User Agent 無條件保存這個 Cookie,並且在 Server 或 Javascript 都可以隨意取得內容,但這不會是身分驗證所期望的。以上例來說,假設 Alice 的登入狀態是 SID=31d4d96e407aad42
,只要有辦法偷到這個已登入狀態,代表可以假造一個帶有已登入狀態的請求,並且是偽造 Alice 的身分哦!
記得!HTTP 是無狀態的,只要請求一模一樣,服務器就無法分辨剛剛「Alice 帶有 Alice 登入狀態的 Cookie 請求」,與「攻擊者帶有 Alice 登入狀態的 Cookie 請求」有何不同,因此會一視同仁地處理並回應。
因此比較理想的狀況是,在 Cookie 的屬性(attribute)上做點存取限制的規格,增加駭客取得此登入狀態的難度,即能提高一定程度的安全性。
RFC 下一個範例即跟 Cookie 存取限制有關,首先先來看 lang
這個 Cookie。它多了兩個屬性為 Domain
與 Path
。
== Server -> User Agent ==
Set-Cookie: lang=en-US; Path=/; Domain=example.com
== User Agent -> Server ==
Cookie: lang=en-US
Domain 屬性的限制規則為:
當 User Agent 收到 Set-Cookie
帶有 Domain 屬性時,會把這個屬性跟請求的 Host 比對。當符合上述條件時 User Agent 才會將 Cookie 保存起來;相同地,當符合上述條件時,User Agent 才會將此 Cookie 帶入 HTTP 請求裡發送給服務器。
只要適當的設置 Domain 為 example.com
,攻擊者就無法使用 attacker.com
來指向同服務器,然後利用瀏覽器信任的特性去取得使用者的 Cookie 狀態。
Path 屬性的限制跟 Domain 類似,規則為:
符合條件時,User Agent 才會將 Cookie 保存起來;相同地,Path 符合條件的話,User Agent 才會將 Cookie 帶入 HTTP 請求裡發送給服務器。
規範有提到,雖然 Path 屬性用來隔離 Cookie,但不能依賴 Path 屬性來保證安全性。
Although seemingly useful for isolating cookies between different paths within a given host, the Path attribute cannot be relied upon for security.
最後來個簡單的舉一反三:如果寫了一個 Path=/
與 Domain=google.com
的 Cookie,則在 drive.google.com
或 mail.google.com
後續的 HTTP 請求都能取得 SID=31d4d96e407aad42
的資料:
== Server -> User Agent ==
Set-Cookie: SID=31d4d96e407aad42; Path=/; Domain=google.com
== User Agent -> Server ==
Cookie: SID=31d4d96e407aad42
另一個範例,則是在示範如何使用 Secure 與 HttpOnly 屬性:
== Server -> User Agent ==
Set-Cookie: SID=31d4d96e407aad42; Path=/; Secure; HttpOnly
== User Agent -> Server ==
Cookie: SID=31d4d96e407aad42
這兩個屬性比較單純:設置了 Secure 的話,代表只有 HTTPS 才能讀與寫;設置了 HttpOnly 則表示禁止 Javascript 讀取此 Cookie。
看完以上的範例,可以大概知道一般身分驗證會全部採用,如:
Set-Cookie: SID=31d4d96e407aad42; Domain=example.com; Expires=Sat, 19-Oct-2019 17:53:50 GMT; Path=/; Secure; HttpOnly
Expires
為過期時間,指時間到該 Cookie 就會自我消毀。
這樣就能確保該 Cookie 能在正常的 Domain 與 Path 下寫入,同時也限制了 HTTPS 與 HttpOnly,駭客要能取得該 Cookie 的難度就會非常高。至於其他應該注意的安全事項,在 RFC 6265 section 8 有更多詳細的說明可以參考。